Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial Rust implementation #174

Merged
merged 28 commits into from
Jan 29, 2025
Merged

Initial Rust implementation #174

merged 28 commits into from
Jan 29, 2025

Conversation

sellout
Copy link
Collaborator

@sellout sellout commented Sep 26, 2024

This provides two more implementations of the ZcashScript trait. One is a pure Rust implementation and the other runs both the C++ and Rust implementations, returning the C++ result and logging a warning if they differ.

This follows the C++ implementation extremely closely for ease of initial review. Future changes will move from this to a Rustier approach.

E.g.,

  • early returns
  • asserts
  • bounds checking followed by accesses1
  • if instead of match
  • mutable refs
  • etc.

It differs from the C++ in some places, as well. Numeric types and casting/From are changed in some places to avoid partiality. Also, the Opcodes are partitioned into multiple types because we eventually want to be able to enforce push_only at the type level, and PushValue operations carry data (although not yet in the structure), which makes them distinct from operations (PushdataBytelength, in particular).

Footnotes

  1. accesses generally return in Result, so ? ensures they’re safe, but we keep the bounds checking so that the stack state matches the C++ version (e.g., without the bounds checking, an op that pops two elements off the stack could fail after popping one, which would leave the stack in a different state than the C++ impl which checks the length of the stack before popping either).

This was referenced Sep 26, 2024
@sellout sellout marked this pull request as draft September 26, 2024 18:33
@sellout
Copy link
Collaborator Author

sellout commented Sep 26, 2024

The files are intended to be compared side-by-side with the C++ implementation. E.g., script.rs first contains definitions from script.h, followed by definitions from script.cpp.

@sellout sellout force-pushed the sellout/rust-interpreter branch from 629eed7 to e95972b Compare September 29, 2024 20:04
@sellout sellout marked this pull request as ready for review September 29, 2024 20:26
@sellout sellout requested a review from nuttycom September 30, 2024 19:02
@sellout
Copy link
Collaborator Author

sellout commented Sep 30, 2024

@daira, @str4d, @ebfull – can’t officially add you as reviewers, but here it is.

The only changes here other than moving chunks of code around are
- moving `evaluate` out of `impl Script`, which required changing
 `&self` to `script: &Script`; and
- unifying `ExecutionOptions` with `VerificationFlags`.
For easier side-by-side comparison.
@sellout sellout force-pushed the sellout/rust-interpreter branch 3 times, most recently from 848ebaa to 09ac96b Compare October 17, 2024 19:54
The C++ impl uses exceptions for `ScriptNum`, but catches them.
These tests run both the C++ and Rust impls. One uses completely
arbitrary inputs, and the other limits script_sig to data pushes.
For now, the underlying errors are discarded when comparing against the
C++ results, but there are corresponding changes on the C++ side in a
separate branch.
@sellout sellout force-pushed the sellout/rust-interpreter branch from 09ac96b to 4c34b3e Compare October 21, 2024 19:35
@sellout
Copy link
Collaborator Author

sellout commented Oct 21, 2024

I don’t know why the test suite is failing on the C++ side now.

  • There are no changes to the C++ in this PR.
  • “It works for me” – locally running cargo test behaves as expected, running both the C++ and Rust tests.

My only guess is that it is somehow caused by the secp256k1(_sys) dependency maybe forcing a different version of the underlying libsecp256k1 to be linked.

Since there’s a lot of noise in the logs, here’s the actual error:

  = note: /usr/bin/ld: /home/runner/work/zcash_script/zcash_script/target/debug/build/zcash_script-13bd32f862a45584/out/libzcash_script.a(484cc929e4b13849-pubkey.o): in function `(anonymous namespace)::Secp256k1SelfTester::Secp256k1SelfTester()':
          /home/runner/work/zcash_script/zcash_script/depend/zcash/src/pubkey.cpp:17: undefined reference to `secp256k1_selftest'
          /usr/bin/ld: /home/runner/work/zcash_script/zcash_script/target/debug/build/zcash_script-13bd32f862a45584/out/libzcash_script.a(484cc929e4b13849-pubkey.o): in function `CPubKey::Verify(uint256 const&, std::vector<unsigned char, std::allocator<unsigned char> > const&) const':
          /home/runner/work/zcash_script/zcash_script/depend/zcash/src/pubkey.cpp:28: undefined reference to `secp256k1_context_static'
          /usr/bin/ld: /home/runner/work/zcash_script/zcash_script/depend/zcash/src/pubkey.cpp:28: undefined reference to `secp256k1_ec_pubkey_parse'
          /usr/bin/ld: /home/runner/work/zcash_script/zcash_script/depend/zcash/src/pubkey.cpp:35: undefined reference to `secp256k1_context_static'
          /usr/bin/ld: /home/runner/work/zcash_script/zcash_script/depend/zcash/src/pubkey.cpp:35: undefined reference to `secp256k1_ecdsa_signature_parse_der'
          /usr/bin/ld: /home/runner/work/zcash_script/zcash_script/depend/zcash/src/pubkey.cpp:40: undefined reference to `secp256k1_context_static'
          /usr/bin/ld: /home/runner/work/zcash_script/zcash_script/depend/zcash/src/pubkey.cpp:40: undefined reference to `secp256k1_ecdsa_signature_normalize'
          /usr/bin/ld: /home/runner/work/zcash_script/zcash_script/depend/zcash/src/pubkey.cpp:41: undefined reference to `secp256k1_context_static'
          /usr/bin/ld: /home/runner/work/zcash_script/zcash_script/depend/zcash/src/pubkey.cpp:41: undefined reference to `secp256k1_ecdsa_verify'
          /usr/bin/ld: /home/runner/work/zcash_script/zcash_script/target/debug/build/zcash_script-13bd32f862a45584/out/libzcash_script.a(484cc929e4b13849-pubkey.o): in function `CPubKey::CheckLowS(std::vector<unsigned char, std::allocator<unsigned char> > const&)':
          /home/runner/work/zcash_script/zcash_script/depend/zcash/src/pubkey.cpp:149: undefined reference to `secp256k1_context_static'
          /usr/bin/ld: /home/runner/work/zcash_script/zcash_script/depend/zcash/src/pubkey.cpp:149: undefined reference to `secp256k1_ecdsa_signature_parse_der'
          /usr/bin/ld: /home/runner/work/zcash_script/zcash_script/depend/zcash/src/pubkey.cpp:152: undefined reference to `secp256k1_context_static'
          /usr/bin/ld: /home/runner/work/zcash_script/zcash_script/depend/zcash/src/pubkey.cpp:152: undefined reference to `secp256k1_ecdsa_signature_normalize'
          collect2: error: ld returned 1 exit status

Copy link
Collaborator

@nuttycom nuttycom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flushing comments; reviewed up to 336d0fe

@conradoplg
Copy link
Contributor

  • “It works for me” – locally running cargo test behaves as expected, running both the C++ and Rust tests.

It works on Rust 1.81 but it doesn't on 1.82. Which is super weird, but should give you a direction where to investigate

- remove `uint256` module
- create a specific type for the C++/Rust comparison implementation
- rename some identifiers
- rephrase some comments
Comment on lines +1222 to +1226
Some((hash_type, vch_sig)) => HashType::from_bits((*hash_type).into(), false)
.ok()
.and_then(|hash_type| (self.sighash)(script_code.0, hash_type))
.map(|sighash| Self::verify_signature(vch_sig, &pubkey, &sighash))
.unwrap_or(false),
Copy link
Contributor

@daira daira Feb 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem right. In the C++, TransactionSignatureChecker::CheckSig calls SignatureHash. In the SIGVERSION_ZIP244 case when txTo.IsCoinBase() || txTo.vin.empty() is false, it throws a std::logic_error in the case of an undefined hash_type (not 0x01, 0x02, 0x03, 0x81, 0x82, or 0x83).

https://github.com/zcash/zcash/blob/a3435336b0c561799ac6805a27993eca3f9656df/src/script/interpreter.cpp#L1237-L1259

HashType::from_bits((*hash_type).into(), false), on the other hand, will only return an error if the two least significant bits are invalid. So there is a divergence in the ZIP 244 case when an undefined bit is set in the hash type.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed the other day, this is a divergence between zcashd’s zcash_script and Zebra’s C++ zcash_script (in this repo), largely due to #157.

The Zebra version doesn’t call SignatureHash.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I don’t think there’s anything to do here – this matches the callback-based C++ as used by Zebra.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that needs to be enforced by the caller, in the callback, since this crate is not aware of whether it's a NU5-onward tx. I'll make a note to double check this in Zebra.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s on the caller to include the StrictEnc flag for NU5 tx1, it doesn’t need to be handled in the callback (and can’t be because the callback receives a HashType, not the bits)2.

If that flag is set, then from_bits gets called with is_strict: true from

fn is_defined_hashtype_signature(vch_sig: &ValType) -> bool {
if vch_sig.is_empty() {
return false;
};
HashType::from_bits(i32::from(vch_sig[vch_sig.len() - 1]), true).is_ok()
}
which causes
let unknown_bits = (bits | 0x83) ^ 0x83;
if is_strict && unknown_bits != 0 {
Err(InvalidHashType::ExtraBitsSet(unknown_bits))
to fail if any bits other than the three we care about are set.

It’s annoying to follow in this match-the-C++ PR, because each call to from_bits has is_strict hardcoded, but in a future commit is_strict is set directly from the StrictEnc flag.

Footnotes

  1. I forget if it even made it into master, but there was a brief period when I reintroduced TxVersion (and a dependency on zcash_primitives) here, so that NU5 transactions would be strict regardless of whether StrictEnc was set, but that was ultimately shot down.

  2. The wrapper used to pass a Rust callback to the C++ implementation explains why it’s ok to be non-strict there https://github.com/ZcashFoundation/zcash_script/blob/2afc4743383686411941fba431f1bf3a8271c059/src/lib.rs#L131-L134

Copy link
Contributor

@daira daira left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed with many comments!

sellout added a commit to sellout/zcash_script that referenced this pull request Feb 13, 2025
Copy link
Collaborator Author

@sellout sellout left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finally addressed @daira’s review. The changes are all up at #197.

.map_err(|_| ScriptError::PubKeyCount)?;
if keys_count > 20 {
return set_error(ScriptError::PubKeyCount);
};
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t know about this one, because

  1. the C++ code doesn’t assert here,
  2. we do check that and return ScriptError::OpCount just below, and
  3. I’m not positive there’s not a bug (in the original) that would result getting here with an invalid op_count that leads to divergent results in the stepper (assertion failure vs ScriptError).

I haven’t done this yet, but it seems like a future change to not expose op_count directly (a future commit does encapsulate it in a State struct), but have an incrementer that returns Err(ScriptError::OpCount) immediately whenever the bound is violated.

Comment on lines +1222 to +1226
Some((hash_type, vch_sig)) => HashType::from_bits((*hash_type).into(), false)
.ok()
.and_then(|hash_type| (self.sighash)(script_code.0, hash_type))
.map(|sighash| Self::verify_signature(vch_sig, &pubkey, &sighash))
.unwrap_or(false),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed the other day, this is a divergence between zcashd’s zcash_script and Zebra’s C++ zcash_script (in this repo), largely due to #157.

The Zebra version doesn’t call SignatureHash.

sellout added a commit to sellout/zcash_script that referenced this pull request Feb 25, 2025
conradoplg pushed a commit that referenced this pull request Feb 25, 2025
* Add ECC & myself (Greg Pfeil) as authors

* Have the Rust impl correctly report “high s” sigs

There is a bug in the C++ side where the error is not set correctly on a
“high s” signature. The Rust side had mirrored this bug, but this
eliminates the bug in the Rust.

* Remove extra byte from sig before low-s check

This doesn’t seem to have any effect on the semantics, as the DER-formatted signature includes
lengths that ensure it will ignore extra bytes, but the C++ code removes the extra byte, so the Rust
should as well.

* Change some comments

Co-authored-by: Daira-Emma Hopwood <[email protected]>

* Appease `rustfmt`

* Have OP_DUP match the C++ impl more closely

* Address the second half of @daira’s #174 review

* Eliminate mutation from `Opcode` parsing

This now splits slices and returns the remaining pieces rather than
modifying the arguments.

* Remove obsolete comment

* Address PR comments

* Address additional comments on #174

---------

Co-authored-by: Daira-Emma Hopwood <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants